/* *
 * --ライセンスについて--
 *
 * 「本ファイルの内容は Mozilla Public License Version 1.1 (「本ライセンス」)
 * の適用を受けます。
 * 本ライセンスに従わない限り本ファイルを使用することはできません。
 * 本ライセンスのコピーは http://www.mozilla.org/MPL/ から入手できます。
 *
 * 本ライセンスに基づき配布されるソフトウェアは、「現状のまま」で配布されるものであり、
 * 明示的か黙示的かを問わず、いかなる種類の保証も行われません。
 * 本ライセンス上の権利および制限を定める具体的な文言は、本ライセンスを参照してください。
 *
 * オリジナルコードおよび初期開発者は、N_H (h.10x64@gmail.com) です。
 *
 * N_H によって作成された部分の著作権表示は次のとおりです。
 *
 * Copyright (C)N_H 2012
 *
 * このファイルの内容は、上記に代えて、
 * GNU General License version2 以降 (以下 GPL とする)、
 * GNU Lesser General Public License Version 2.1 以降 (以下 LGPL とする)、
 * の条件に従って使用することも可能です。
 * この場合、このファイルの使用には上記の条項ではなく GPL または LGPL の条項が適用されます。
 * このファイルの他者による使用を GPL または LGPL の条件によってのみ許可し、
 * MPL による使用を許可したくない対象者は、上記の条項を削除することでその意思を示し、
 * 上記条項を GPL または LGPL で義務付けられている告知およびその他の条項に置き換えてください。
 * 対象者が上記の条項を削除しない場合、
 * 受領者は MPL または GPL または LGPL ライセンスのいずれによってもこのファイルを
 * 使用することができます。」
 *
 * -- License --
 *
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License。You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND、either express or implied。See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Initial Developer of the Original Code is
 *   N_H (h.10x64@gmail.com).
 *
 * Portions created by the Initial Developer are Copyright (C)N_H 2012
 * the Initial Developer。All Rights Reserved.
 *
 * Alternatively、the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL")、or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above。If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL、and not to allow others to
 * use your version of this file under the terms of the MPL、indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL。If you do not delete
 * the provisions above、a recipient may use your version of this file under
 * the terms of any one of the MPL、the GPL or the LGPL.
 *
 * */
package com.magiciansforest.audio.soundrenderer.logic.sound;

import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.magiciansforest.audio.file.wav.WavFile;
import com.magiciansforest.audio.soundrenderer.logic.TimeKeeper;
import java.io.File;
import java.io.IOException;
import java.util.TreeMap;
import javax.sound.sampled.AudioFormat;

/**
 *
 * @author N_H
 */
public class SoundSource extends Node {

    public static final int PLAYING = 0;
    public static final int PAUSED = 1;
    public static final int STOPPED = 2;
    private long id;
    private AudioRenderThread renderer;
    private BinauralSoundFilter binauralFilter;
    private TreeMap<Double, Vector3f> posLog = new TreeMap<Double, Vector3f>();
    private boolean doesSoundArrived;
    private AudioFormat outputFormat;
    private long startFrame = 0, endFrame = 0;
    private double listenerSpeed;
    private double sourceSpeed;
    private double readBytes;
    private double lastUpdate;
    private double[] soundBuffer;
    private int status = PAUSED;
    private double volume = 1;
    private WavFile wav = null;
    private double time = 0;

    public SoundSource(long id, AudioRenderThread parent) {
        super("SoundSource" + id);
        this.id = id;
        this.renderer = parent;
        this.outputFormat = parent.getOutputFormat();

        binauralFilter = new BinauralSoundFilter(parent.getHRTF(), parent.getAir(), parent.getOutputFormat().getSampleRate());
        lastUpdate = TimeKeeper.getTimeKeeper().getTime();
        soundBuffer = new double[0];
    }

    public long getID() {
        return id;
    }

    public BinauralSoundFilter getBinauralFilter() {
        return binauralFilter;
    }

    public void setWavFile(File f) throws IOException {
        if (wav != null) {
            wav.close();
        }

        this.wav = WavFile.createReadOnlyWavFile(f);
    }

    public String getWavFilePath() {
        if (wav == null) {
            return null;
        }

        return wav.getFilePath();
    }

    public boolean hasWavData() {
        return (wav != null);
    }

    public int getStatus() {
        return status;
    }

    public void play() {
        status = PLAYING;
    }

    public void stop() {
        status = PAUSED;
    }

    public void pause() {
        status = PAUSED;
    }

    public long getEndFrame() {
        return endFrame;
    }

    public void setEndFrame(long endFrame) {
        this.endFrame = endFrame;
    }

    public long getStartFrame() {
        return startFrame;
    }

    public void setStartFrame(long startFrame) {
        this.startFrame = startFrame;
    }

    public void setFrame(long frame) throws IOException {
        if (wav != null) {
            long pos = frame * wav.getFormatChunk().getChannels() * wav.getFormatChunk().getBitsPerSample() / 8;
            wav.seek(pos);
            readBytes = pos;
        }

        time = 0;
        posLog.clear();
        doesSoundArrived = false;
        lastUpdate = time;
        soundBuffer = new double[0];
    }

    public long getLastFrameNo() {
        if (wav == null) {
            return -1;
        }

        return wav.getDataLength() / (wav.getFormatChunk().getChannels() * wav.getFormatChunk().getBitsPerSample() / 8);
    }

    public int getSampleRate() {
        if (wav == null) {
            return 44100;
        } else {
            return (int) wav.getFormatChunk().getSamplingRate();
        }
    }

    public double getVolume() {
        return volume;
    }

    public void setVolume(double vol) {
        this.volume = vol;
    }

    public void update(Vector3f listenerPos, Quaternion listenerRot, double dt) {
        if (status == PLAYING) {
            if (dt > 0) {
                lastUpdate = time;

                if (posLog.size() == 0 || !posLog.lastEntry().getValue().equals(getWorldTranslation())) {
                    posLog.put(time, getWorldTranslation().clone());
                }

                double sourceTime = calcCollideTime(listenerPos, time, dt, 1.0/outputFormat.getSampleRate());

                if (doesSoundArrived) {
                    while (posLog.size() > 1 && posLog.firstKey() < sourceTime - 1) {
                        posLog.pollFirstEntry();
                    }
                } else {
                    double startTime = posLog.firstKey();
                    if (renderer.getAir().getSoundSpeed() * (time - startTime) >= posLog.get(startTime).subtract(listenerPos).length() * renderer.getUnitSize()) {
                        doesSoundArrived = true;
                    }
                }

                Vector3f listenerSpeed_v = listenerPos.subtract(renderer.getLastListenerPos()).mult((float) (1.0 / dt));

                Vector3f sourcePosition = getPosition(sourceTime);
                Vector3f sourceSpeed_v = getSpeed(sourceTime);
                Vector3f speedDir = sourcePosition.subtract(listenerPos).normalize();

                listenerSpeed = (listenerSpeed_v.length() == 0) ? 0 : listenerSpeed_v.dot(speedDir) * renderer.getUnitSize();
                sourceSpeed = (sourceSpeed_v.length() == 0) ? 0 : sourceSpeed_v.dot(speedDir) * renderer.getUnitSize();
                //System.out.println("src:" + sourceSpeed);

                sourcePosition = sourcePosition.subtract(listenerPos).mult((float) renderer.getUnitSize());
                sourcePosition = listenerRot.inverse().mult(sourcePosition);
                if (!sourcePosition.equals(Vector3f.ZERO)) {
                    binauralFilter.setSoundPosition_v(sourcePosition.z, sourcePosition.y, -sourcePosition.x);
                } else {
                    binauralFilter.setSoundPosition_s(0, 0, 1);
                }
            }
            
            time += dt;
        }
    }

    public int read(double[] dest) {
        if (wav == null) {
            throw new IllegalStateException("wav == null");
        }
        if (status == STOPPED && soundBuffer.length == 0) {
            return 0;
        }

        if (!doesSoundArrived) {
            for (int i = 0; i < dest.length; i++) {
                dest[i] = 0;
            }

            return dest.length * wav.getFormatChunk().getBitsPerSample() / 8;
        } else {
            int startBufLen = soundBuffer.length;

            int readDataLength = 0;
            while ((soundBuffer.length - startBufLen) <= 0 || soundBuffer.length < dest.length) {
                int dopplerLength = (int) Math.round(dest.length * calcDopplerRatio(listenerSpeed, sourceSpeed));

                double[] dataBuf = new double[dest.length];

                //Render
                //ReadData
                if (status != PAUSED && status != STOPPED) {
                    readDataLength = readData(dataBuf);
                } else {
                    for (int i = 0; i < dataBuf.length; i++) {
                        dataBuf[i] = 0;
                    }
                }

                //Effect
                double[] renderedBuf = doppler(dataBuf, dopplerLength);

                //Add rendered data into the soundBuffer
                if (soundBuffer.length > 0) {
                    double[] next = new double[soundBuffer.length + renderedBuf.length];
                    System.arraycopy(soundBuffer, 0, next, 0, soundBuffer.length);
                    System.arraycopy(renderedBuf, 0, next, soundBuffer.length, renderedBuf.length);
                    soundBuffer = next;
                } else {
                    soundBuffer = renderedBuf;
                }

                if (readDataLength <= 0) {
                    break;
                }
            }

            //Stop reading if readBytes >= dataLength
            readBytes += readDataLength;
            if (readBytes >= wav.getDataLength() || readDataLength <= 0) {
                stop();
            }

            //Output
            int ret;
            if (soundBuffer.length >= dest.length) {
                for (int i = 0; i < dest.length; i++) {
                    dest[i] = soundBuffer[i];
                }
                ret = dest.length * wav.getFormatChunk().getBitsPerSample() / 8;
            } else {
                for (int i = 0; i < soundBuffer.length; i++) {
                    dest[i] = soundBuffer[i];
                }
                for (int i = soundBuffer.length; i < dest.length; i++) {
                    dest[i] = 0;
                }
                ret = soundBuffer.length * wav.getFormatChunk().getBitsPerSample() / 8;
            }

            //Remove outputted data
            if (soundBuffer.length <= dest.length) {
                soundBuffer = new double[0];
            } else {
                double[] next = new double[soundBuffer.length - dest.length];
                System.arraycopy(soundBuffer, dest.length, next, 0, next.length);
                soundBuffer = next;
            }

            return ret;
        }
    }

    private double calcDopplerRatio(double listenerSpeed, double sourceSpeed) {
        double soundSpeed = renderer.getAir().getSoundSpeed();
        double max = soundSpeed - 0.1 * soundSpeed; // 90% of sound speed
        double obsSpeed = Math.max(-max, Math.min(max, listenerSpeed));
        double srcSpeed = Math.max(-max, Math.min(max, sourceSpeed));
        double ret = (soundSpeed - obsSpeed) / (soundSpeed - srcSpeed);
        return ret;
    }

    private double[] doppler(double[] data, int length) {
        double[] ret = new double[length];

        double dataFrameLength = data.length;
        for (int i = 0; i < length; i++) {
            double pos = (double) i / (length - 1);
            double index_d = (dataFrameLength - 1) * pos;
            int hIndex = (int) Math.ceil(index_d), lIndex = (int) Math.floor(index_d);
            if (hIndex == lIndex) {
                ret[i] = data[hIndex];
            } else {
                double ratio = 0.5 - 0.5 * Math.cos((index_d - lIndex) * Math.PI);
                ret[i] = data[hIndex] * ratio + data[lIndex] * (1.0 - ratio);
            }
        }

        return ret;
    }

    private int readData(double[] dest) {
        if (wav == null) {
            throw new IllegalStateException("wav == null");
        } else {
            int bytesPerSample = wav.getFormatChunk().getBitsPerSample() / 8;
            byte[] buffer = new byte[wav.getFormatChunk().getChannels() * dest.length * bytesPerSample];

            int ret;
            try {
                ret = wav.read(buffer);
            } catch (IOException ioe) {
                ioe.printStackTrace();
                ret = -1;
                for (int i = 0; i < buffer.length; i++) {
                    buffer[i] = 0;
                }
            }
            toDouble(dest, buffer, volume, wav.getFormatChunk().getChannels(), wav.getFormatChunk().getBitsPerSample(), false);

            return ret;
        }
    }

    private Vector3f getPosition(double t) {
        if (t < posLog.firstKey()) {
            return posLog.firstEntry().getValue();
        } else if (t > posLog.lastKey()) {
            return posLog.lastEntry().getValue();
        } else if (posLog.containsKey(t)) {
            return posLog.get(t);
        }

        double lt = posLog.floorKey(t), ht = posLog.ceilingKey(t);

        Vector3f lPos = posLog.get(lt), hPos = posLog.get(ht);
        double ratio = (t - lt) / (ht - lt);
        Vector3f ret = lPos.mult((float) (1.0 - ratio)).add(hPos.mult((float) ratio));

        return ret;
    }

    private Vector3f getSpeed(double t) {
        if (posLog.size() <= 1) {
            return new Vector3f(0, 0, 0);
        }
        if (t < posLog.firstKey()) {
            return new Vector3f(0, 0, 0);
        } else if (posLog.firstKey() == t) {
            return new Vector3f(0, 0, 0);
        }

        double t1, t0, dt;
        if (t > posLog.lastKey()) {
            t1 = posLog.lastKey();
            t0 = posLog.lowerKey(t1);
        } else if (posLog.containsKey(t)) {
            t1 = t;
            t0 = posLog.lowerKey(t);
        } else {
            t1 = posLog.ceilingKey(t);
            t0 = posLog.floorKey(t);
        }
        dt = t1 - t0;

        return posLog.get(t1).subtract(posLog.get(t0)).mult((float) (1.0 / dt));
    }

    private double calcCollideTime(Vector3f listenerPos, double now, double dt, double acculacy) {
        int count = 0;
        double f, df;
        double t = now;
        while (Math.abs((f = collideTime(listenerPos, t) - now)) > acculacy && count < 100) {
            df = (collideTime(listenerPos, t) - collideTime(listenerPos, t - dt)) / dt;
            t = t - f / df;
            count++;
        }
        return t;
    }

    private double collideTime(Vector3f listenerPos, double ts) {
        double tsource = ts;
        double dist = getPosition(tsource).subtract(listenerPos).length() * renderer.getUnitSize();
        return dist / renderer.getAir().getSoundSpeed() + tsource;
    }

    private static void toDouble(double[] dest, byte[] data, double volume, int inputChannels, int bitRate, boolean isBigEndian) {
        int byteRate = bitRate / 8;

        double[] dataBuf = new double[data.length / byteRate];
        for (int i = 0; i < dataBuf.length; i++) {
            long bits = 0x0000000000000000l;
            if (isBigEndian) {
                for (int j = 0; j < byteRate; j++) {
                    bits |= (data[i * byteRate + j] & 0xFF) << (8 * (byteRate - j - 1));
                }
            } else {
                for (int j = 0; j < byteRate; j++) {
                    bits |= (data[i * byteRate + j] & 0xFF) << (8 * j);
                }
            }

            switch (byteRate) {
                case 2:
                    dataBuf[i] = (short) (bits & 0xFFFFl);
                    break;
                case 3:
                    dataBuf[i] = (int) (bits & 0xFFFFFFl);
                    break;
                case 4:
                    dataBuf[i] = Float.intBitsToFloat((int) (bits & 0xFFFFFFFFl));
                    break;
                case 8:
                    dataBuf[i] = Double.longBitsToDouble(bits);
                    break;
                default:
                    dataBuf[i] = data[i];
                    break;
            }
        }

        if (inputChannels <= 1) {
            for (int i = 0; i < dest.length; i++) {
                dest[i] = volume * dataBuf[i];
            }
        } else {
            for (int i = 0; i < dest.length; i++) {
                for (int ch = 0; ch < inputChannels; ch++) {
                    dest[i] += volume * dataBuf[inputChannels * i + ch] / inputChannels;
                }
            }
        }
    }
}